Day 8 建立了 LocalStorage 和 SecureStorage 的分層架構。今天,我們將一起來看看三個重要的層面:
在開發過程中,我們發現這些問題都需要系統化的解決方案。本文分享我們在 Crew Up!
專案中的實際落地經驗,所有程式碼範例都可以在專案中找到對應的實作。
🎯 Cache Key 命名規則
專案中實際使用的模組化命名格式:
{feature}:{dataType}[:{identifier}]
實際使用範例:
activity:list
:活動清單(ActivityRepository)activity:detail:{activityId}
:活動詳情(ActivityRepository)activity:categories
:活動分類(ActivityRepository)home:indexActivities
:首頁活動清單(IndexRepository)home:popularActivities
:首頁熱門活動清單(PopularActivityRepository)home:activeCrewers
:首頁活躍 Crewer(ActiveCrewerRepository)profile:user:{userId}
:使用者 Profile(ProfileRepository)💡 設計優勢:模組化命名讓 feature-first 架構中的快取管理更加清晰,前綴清理可批次清除特定模組快取,例如清除所有
home:
相關快取。
基於 Day 8 建立的分層儲存架構,我們在 LocalStorage 層實作了 TTL 能力,將實際資料與快取中繼資料一起管理:
💡 與 Day 8 的關聯:Day 8 建立了 LocalStorage(非敏感資料)和 SecureStorage(敏感資料)的分層架構。Day 9 的 TTL 功能專門增強 LocalStorage 的快取能力,嚴格遵循安全邊界原則。
🔧 資料結構設計:
value
:實際資料(以字串儲存,JSON 序列化)cachedAt
:寫入時間戳ttlMs
:存活時間(毫秒)核心類別實作:
// lib/app/core/storage/cache_item.dart
// (imports omitted)
class CacheItem {
final String value;
final DateTime cachedAt;
final int ttlMs;
// ... isExpired、remainingMs、toJson/fromJson 等
}
LocalStorage 介面擴充:
// lib/app/core/storage/local_storage.dart
// (imports omitted)
/// 本地儲存抽象介面(Day 8 基礎 + Day 9 TTL 擴充)
///
/// 在 Day 8 建立的基礎儲存介面上,新增 TTL 快取能力
abstract class LocalStorage {
// Day 8 的基本儲存操作
Future<void> saveString(String key, String value);
Future<String?> getString(String key);
Future<void> saveBool(String key, bool value);
Future<bool?> getBool(String key);
Future<void> saveInt(String key, int value);
Future<int?> getInt(String key);
Future<void> saveDouble(String key, double value);
Future<double?> getDouble(String key);
Future<void> saveStringList(String key, List<String> value);
Future<List<String>?> getStringList(String key);
Future<bool> containsKey(String key);
Future<void> remove(String key);
Future<void> clear();
Future<Set<String>> getKeys();
// Day 9 新增的 TTL 相關方法
Future<void> saveStringWithTTL(String key, String value, Duration ttl);
Future<String?> getStringWithTTL(String key);
Future<bool> isExpired(String key);
Future<void> evictExpired();
Future<CacheItem?> getCacheMetadata(String key);
}
快取管理方法整合:
LocalStorage 已整合實用的快取管理方法,無需額外的輔助類別:
// LocalStorage 介面已包含所有必要的 TTL 操作
abstract class LocalStorage {
// 基本 TTL 操作
Future<void> saveStringWithTTL(String key, String value, Duration ttl);
Future<String?> getStringWithTTL(String key);
Future<bool> isExpired(String key);
Future<void> evictExpired();
// 進階快取管理
Future<CacheItem?> getCacheMetadata(String key);
Future<bool> isCacheValid(String key);
Future<int> getRemainingTime(String key);
Future<void> evictExpiredByPrefix(String prefix);
}
🎯 Feature-Specific Cache Config
各 feature 模組的實際快取設定:
// lib/features/activity/data/activity_cache_config.dart
// (imports omitted)
/// Activity module cache configuration
class ActivityCacheConfig {
// Cache Keys
static const String activityListKey = 'activity:list';
static const String activityDetailKeyPrefix = 'activity:detail:';
static const String categoriesKey = 'activity:categories';
// TTL Constants
static const Duration activityListTTL = Duration(minutes: 30);
static const Duration activityDetailTTL = Duration(hours: 1);
static const Duration categoriesTTL = Duration(hours: 4);
// Helper Methods
static String activityDetailKey(String activityId) =>
'$activityDetailKeyPrefix$activityId';
}
在開發過程中,發現將 TTL 與 Cache 策略整合進 BaseRepository
是一個好的作法,可以提供一致且可測試的執行流程。
// lib/app/core/repositories/base_repository.dart
// (imports omitted)
/// TTL 快取策略(核心抽象)
enum CacheStrategy {
none,
cacheFirst,
remoteFirstWithCache,
localFirstWithCache
}
/// 基礎 Repository 類別
///
/// 提供統一的錯誤處理和基礎功能,遵循 feature-first 原則
/// 不包含具體的業務邏輯,只提供基礎設施
class BaseRepository {
final String repositoryName;
final LocalStorage? _localStorage;
const BaseRepository(this.repositoryName, [this._localStorage]);
/// 執行帶有 TTL 快取的統一介面
///
/// 這是核心層提供的基礎設施,具體策略由各 feature 的 repository 實作
Future<Result<T>> executeWithStrategy<T>(
String operation,
RepositoryOperation operationType,
CacheStrategy cacheStrategy,
String cacheKey,
Duration ttl,
Future<T> Function() primarySource, {
Future<T> Function()? fallbackSource,
String Function(T)? serializer,
T Function(String)? deserializer,
}) async {
// 核心層只提供統一的框架,不包含具體實作
// 具體的快取策略由各 feature 的組合策略實作
}
}
📝 快取策略使用場景:
🎯 Feature-First 架構下的組合模式設計:
遵循 feature-first 原則,採用組合模式來提供共用功能,同時保持職責分離:
// lib/app/core/repositories/cache_strategy.dart
/// 快取策略類別
///
/// 專門處理快取相關邏輯,與錯誤處理策略分離
class CacheStrategy {
final String repositoryName;
final LocalStorage? _localStorage;
const CacheStrategy(this.repositoryName, this._localStorage);
Future<Result<T>> executeRemoteFirst<T>(
String operation,
String cacheKey,
Duration ttl,
Future<T> Function() remoteSource, {
Future<T> Function()? fallbackSource,
String Function(T)? serializer,
T Function(String)? deserializer,
}) async { /* 實作細節 */ }
Future<Result<T>> executeCacheFirst<T>(
String operation,
String cacheKey,
Duration ttl,
Future<T> Function() primarySource, {
Future<T> Function()? fallbackSource,
String Function(T)? serializer,
T Function(String)? deserializer,
}) async { /* 實作細節 */ }
/// 執行 Stale-While-Revalidate 策略
Future<Result<T>> executeStaleWhileRevalidate<T>(
String operation,
String cacheKey,
Duration ttl,
Future<T> Function() primarySource, {
Future<T> Function()? fallbackSource,
String Function(T)? serializer,
T Function(String)? deserializer,
}) async { /* 實作細節 */ }
}
// lib/app/core/repositories/enhanced_base_repository.dart
/// 增強版基礎 Repository 類別
class EnhancedBaseRepository extends BaseRepository {
late final cache.CacheStrategy _cacheStrategy;
EnhancedBaseRepository(String repositoryName, [LocalStorage? localStorage])
: super(repositoryName, localStorage) {
_cacheStrategy = cache.CacheStrategy(repositoryName, localStorage);
}
/// 執行遠端優先快取策略
Future<Result<T>> executeRemoteFirstWithCache<T>(
String operation,
String cacheKey,
Duration ttl,
Future<T> Function() remoteSource, {
Future<T> Function()? fallbackSource,
String Function(T)? serializer,
T Function(String)? deserializer,
}) => _cacheStrategy.executeRemoteFirst(
operation, cacheKey, ttl, remoteSource,
fallbackSource: fallbackSource,
serializer: serializer,
deserializer: deserializer,
);
Future<Result<T>> executeCacheFirst<T>(
String operation,
String cacheKey,
Duration ttl,
Future<T> Function() primarySource, {
Future<T> Function()? fallbackSource,
String Function(T)? serializer,
T Function(String)? deserializer,
}) => _cacheStrategy.executeCacheFirst(
operation, cacheKey, ttl, primarySource,
fallbackSource: fallbackSource,
serializer: serializer,
deserializer: deserializer,
);
/// 執行 Stale-While-Revalidate 策略
Future<Result<T>> executeStaleWhileRevalidate<T>(
String operation,
String cacheKey,
Duration ttl,
Future<T> Function() primarySource, {
Future<T> Function()? fallbackSource,
String Function(T)? serializer,
T Function(String)? deserializer,
}) => _cacheStrategy.executeStaleWhileRevalidate(
operation, cacheKey, ttl, primarySource,
fallbackSource: fallbackSource,
serializer: serializer,
deserializer: deserializer,
);
}
✅ Feature-First 架構原則:
CacheStrategy
處理快取邏輯EnhancedBaseRepository
整合快取策略類別以下展示核心的 TTL 快取實作,每個 feature 使用自己的 cache config 並透過增強繼承模式享受統一的快取能力:
// lib/features/activity/data/repositories/activity_repository_impl.dart
class ActivityRepositoryImpl extends EnhancedBaseRepository
implements ActivityRepository {
@override
Future<Result<List<Activity>>> getActivities() async =>
executeRemoteFirstWithCache( // 先打遠端,失敗時用快取
'Fetching ${ActivityCacheConfig.activityListKey}',
ActivityCacheConfig.activityListKey,
ActivityCacheConfig.activityListTTL, // 30分鐘 TTL
() async {
final remoteActivities = await _remoteDataSource.getAllActivities();
// 成功後同步到本地
await executeSafeSync('Syncing activities to local', () async {
for (final activity in remoteActivities) {
await _localDataSource.saveActivity(activity);
}
});
return remoteActivities;
},
fallbackSource: () async => await _localDataSource.getAllActivities(),
serializer: (activities) =>
jsonEncode(activities.map((a) => a.toJson()).toList()),
deserializer: (jsonString) {
final List<dynamic> jsonList = jsonDecode(jsonString);
return jsonList.map((json) => Activity.fromJson(json)).toList();
},
);
}
// lib/features/home/data/repositories/index_repository_impl.dart
class IndexRepositoryImpl extends EnhancedBaseRepository
implements IndexRepository {
@override
Future<Result<List<Activity>>> getIndexActivities() async =>
executeRemoteFirstWithCache<List<Activity>>( // 先打遠端,失敗時用快取
'Fetching ${HomeCacheConfig.indexActivitiesKey}',
HomeCacheConfig.indexActivitiesKey,
HomeCacheConfig.indexDataTTL, // 20分鐘 TTL
() async => await _remoteDataSource.getIndexActivities(),
fallbackSource: () async => await _localDataSource.getIndexActivities(),
serializer: (activities) =>
jsonEncode(activities.map((a) => a.toJson()).toList()),
deserializer: (jsonString) {
final List<dynamic> jsonList = jsonDecode(jsonString);
return jsonList.map((json) => Activity.fromJson(json)).toList();
},
);
}
// lib/features/home/data/repositories/active_crewer_repository_impl.dart
class ActiveCrewerRepositoryImpl extends EnhancedBaseRepository
implements ActiveCrewerRepository {
@override
Future<Result<List<ActiveCrewer>>> getActiveCrewers() async =>
executeStaleWhileRevalidate<List<ActiveCrewer>>( // 立即返回快取,背景更新
'Fetching ${HomeCacheConfig.activeCrewersKey}',
HomeCacheConfig.activeCrewersKey,
HomeCacheConfig.activeCrewersTTL, // 15分鐘 TTL
() async => await _remoteDataSource.getActiveCrewers(),
fallbackSource: () async => await _localDataSource.getActiveCrewers(),
serializer: (activeCrewers) =>
jsonEncode(activeCrewers.map((c) => c.toJson()).toList()),
deserializer: (jsonString) {
final List<dynamic> jsonList = jsonDecode(jsonString);
return jsonList.map((json) => ActiveCrewer.fromJson(json)).toList();
},
);
}
📋 不同策略的使用場景:
📊 Feature-First 架構的 TTL 快取總結:
app/core/
)提供基礎設施,feature 層實作具體業務邏輯目標:將舊的 home:
快取清空(含對應 metadata),並保留其他資料。
// lib/app/core/storage/local_storage.dart
Future<void> _migrateData(int from, int to) async {
developer.log('📦 執行資料遷移: v$from -> v$to', name: 'LocalStorageImpl');
if (from == 1 && to >= 2) {
await _removeByPrefix('home:');
return;
}
if (from == 0 && to >= 1) {
return;
}
developer.log('⚠️ 未知的儲存版本,清空快取', name: 'LocalStorageImpl');
await _preferences!.clear();
await _preferences!.setInt(_storageVersionKey, _currentStorageVersion);
}
Future<void> _removeByPrefix(String prefix) async {
final keys = _preferences!.getKeys();
for (final key in keys) {
if (key.startsWith(prefix)) {
await _preferences!.remove(key);
final metadataKey = '${key}_metadata';
if (keys.contains(metadataKey)) {
await _preferences!.remove(metadataKey);
}
}
}
}
✅ 原則:從一開始就要建立版本,既使還沒有要做遷移,也要未雨綢繆。
今天的 TTL 快取實作體現了幾個重要的設計原則:
明天,我們將深入探討「錯誤處理與日誌記錄」,學習如何建立完整的錯誤追蹤機制、統一的異常處理策略,以及可觀測的系統日誌,讓開發和維護過程更加順暢。
期待與您在 Day 10 相見!